---
title: Context API
---

The Context API provides direct access to assistant-ui's state management system, enabling you to build custom components that integrate seamlessly with the assistant runtime.

## Introduction

The Context API is assistant-ui's powerful state management system that enables you to build custom components with full access to the assistant's state and capabilities. It provides:

- **Reactive state access** - Subscribe to state changes with automatic re-renders
- **Action execution** - Trigger operations like sending messages or reloading responses
- **Event listening** - React to user interactions and system events
- **Scope-aware design** - Components automatically know their context (message, thread, etc.)

It's the foundation that powers all assistant-ui primitives. When the built-in components don't meet your needs, you can use the Context API to create custom components with the same capabilities.

The Context API is backed by the runtime you provide to `<AssistantRuntimeProvider>`. This runtime acts as a unified store that manages all assistant state, handles actions, and dispatches events across your entire application.

## Core Concepts

### Scopes and Hierarchy

assistant-ui organizes state into **scopes** - logical boundaries that provide access to relevant data and actions. Each scope corresponds to a specific part of the chat interface and automatically provides context-aware functionality.

```
🗂️  ThreadList (threads) - Manages the list of conversations
    ├── 📄 ThreadListItem (threadListItem) - Individual thread in the list
    └── 💬 Thread (thread) - Active conversation with messages
        ├── 🔵 Message (message) - User or assistant message
        │   ├── 📝 Part (part) - Content within a message (text, tool calls, etc.)
        │   ├── 📎 Attachment (attachment) - Files attached to messages
        │   └── ✏️  Composer (composer) - Edit mode for existing messages
        │       └── 📎 Attachment (attachment) - Files in edit mode
        └── ✏️  Composer (composer) - New message input
            └── 📎 Attachment (attachment) - Files being added

🔧 Tools (tools) - Custom UI components for tool calls
```

**How scopes work:**

- Scopes are **automatically determined** by where your component is rendered
- A button inside a `<ThreadPrimitive.Messages>` automatically gets `message` scope
- A button inside a `<ComposerPrimitive.Attachments>` automatically gets `attachment` scope
- Child scopes can access parent scope data (e.g., a `message` component can access `thread` data)

```tsx
// Inside a message component
function MessageButton() {
  // ✅ Available: message scope (current message)
  const role = useAssistantState(({ message }) => message.role);

  // ✅ Available: thread scope (parent)
  const isRunning = useAssistantState(({ thread }) => thread.isRunning);
}
```

### State Management Model

The Context API follows a predictable state management pattern:

1. **State** is immutable and flows down through scopes
2. **Actions** are methods that trigger state changes
3. **Events** notify components of state changes and user interactions
4. **Subscriptions** let components react to changes

## Essential Hooks

### useAssistantState

Read state reactively with automatic re-renders when values change. This hook works like Zustand's selector pattern - you provide a function that extracts the specific data you need, and your component only re-renders when that data changes.

```tsx
import { useAssistantState } from "@assistant-ui/react";

// Basic usage - extract a single property
const role = useAssistantState(({ message }) => message.role); // "user" | "assistant"
const isRunning = useAssistantState(({ thread }) => thread.isRunning); // boolean

// Access nested data
const attachmentCount = useAssistantState(
  ({ composer }) => composer.attachments.length,
);
const lastMessage = useAssistantState(({ thread }) => thread.messages.at(-1));
```

The selector function receives all available scopes for your component's location and should return a specific value. The component re-renders only when that returned value changes.

**Common patterns:**

```tsx
// Access multiple scopes
const canSend = useAssistantState(
  ({ thread, composer }) => !thread.isRunning && composer.text.length > 0,
);

// Compute derived state
const messageCount = useAssistantState(({ thread }) => thread.messages.length);
```

**Important:** Never create new objects in selectors. Return primitive values or stable references to avoid infinite re-renders.

```tsx
// ❌ Bad - creates new object every time
const data = useAssistantState(({ message }) => ({
  role: message.role,
  content: message.content,
}));

// ✅ Good - returns stable values
const role = useAssistantState(({ message }) => message.role);
const content = useAssistantState(({ message }) => message.content);
```

### useAssistantApi

Access the API instance for imperative operations and actions. Unlike `useAssistantState`, this hook returns a stable object that never changes, making it perfect for event handlers and imperative operations.

```tsx
import { useAssistantApi } from "@assistant-ui/react";

function CustomMessageActions() {
  const api = useAssistantApi();

  // Perform actions in event handlers
  const handleSend = () => {
    api.composer().send();
  };

  const handleReload = () => {
    api.message().reload();
  };

  // Read state imperatively when needed
  const handleConditionalAction = () => {
    const { isRunning } = api.thread().getState();
    const { text } = api.composer().getState();

    if (!isRunning && text.length > 0) {
      api.composer().send();
    }
  };

  return (
    <div>
      <button onClick={handleSend}>Send</button>
      <button onClick={handleReload}>Reload</button>
      <button onClick={handleConditionalAction}>Smart Send</button>
    </div>
  );
}
```

The API object is stable and doesn't cause re-renders. Use it for:

- **Triggering actions** in event handlers and callbacks
- **Reading current state** imperatively when you don't need subscriptions
- **Accessing nested scopes** programmatically
- **Checking scope availability** before performing actions

**Available actions by scope:**

```tsx
// Thread actions
api.thread().append(message);
api.thread().startRun(config);
api.thread().cancelRun();
api.thread().switchToNewThread();
api.thread().switchToThread(threadId);
api.thread().getState();
api.thread().message(idOrIndex);
api.thread().composer;

// Message actions
api.message().reload();
api.message().speak();
api.message().stopSpeaking();
api.message().submitFeedback({ type: "positive" | "negative" });
api.message().switchToBranch({ position, branchId });
api.message().getState();
api.message().part(indexOrToolCallId);
api.message().composer;

// Part actions
api.part().addResult(result);
api.part().getState();

// Composer actions
api.composer().send();
api.composer().setText(text);
api.composer().setRole(role);
api.composer().addAttachment(file);
api.composer().clearAttachments();
api.composer().reset();
api.composer().getState();

// Attachment actions
api.attachment().remove();
api.attachment().getState();

// ThreadList actions
api.threads().switchToNewThread();
api.threads().switchToThread(threadId);
api.threads().getState();

// ThreadListItem actions
api.threadListItem().switchTo();
api.threadListItem().rename(title);
api.threadListItem().archive();
api.threadListItem().unarchive();
api.threadListItem().delete();
api.threads().getState();

// Tools actions
api.tools().setToolUI(toolName, render);
api.tools().getState();
```

### useAssistantEvent

Subscribe to events with automatic cleanup on unmount. This hook is perfect for reacting to user interactions, system events, or integrating with external analytics.

```tsx
import { useAssistantEvent } from "@assistant-ui/react";

// Listen to current scope events (most common)
useAssistantEvent("composer.send", (event) => {
  console.log("Current composer sent message:", event.message);
});

// Listen to all events of a type across all scopes
useAssistantEvent({ event: "composer.send", scope: "*" }, (event) => {
  console.log("Any composer sent a message:", event);
});

// Listen to ALL events (useful for debugging or analytics)
useAssistantEvent("*", (event) => {
  console.log("Event occurred:", event.type, "from:", event.source);
  // Send to analytics, logging, etc.
});

// Practical example: Track user interactions
function AnalyticsTracker() {
  useAssistantEvent("composer.send", (event) => {
    analytics.track("message_sent", {
      messageLength: event.message.content.length,
      hasAttachments: event.message.attachments.length > 0,
    });
  });

  return null; // This component only tracks events
}
```

**Event name patterns:**

- Event names follow `source.action` format (e.g., `composer.send`, `thread.run-start`)
- Use `"*"` as the event name to listen to all events
- The `scope` parameter controls which instances trigger the event

## Working with Scopes

### Available Scopes

Each scope provides access to specific state and actions:

- **ThreadList** (`threads`): Collection and management of threads
- **ThreadListItem** (`threadListItem`): Individual thread in the list
- **Thread** (`thread`): Conversation with messages
- **Message** (`message`): Individual message (user or assistant)
- **Part** (`part`): Content part within a message (text, tool calls, etc.)
- **Composer** (`composer`): Text input for sending or editing messages
- **Attachment** (`attachment`): File or media attached to a message or composer
- **Tools** (`tools`): Tool UI components

### Scope Resolution

The Context API automatically resolves the current scope based on component location:

```tsx
function MessageButton() {
  const api = useAssistantApi();

  // Automatically uses the current message scope
  const handleReload = () => {
    api.message().reload();
  };

  return <button onClick={handleReload}>Reload</button>;
}
```

### Checking Scope Availability

Before accessing a scope, check if it's available:

```tsx
const api = useAssistantApi();

// Check if message scope exists
if (api.message.source) {
  // Safe to use message scope
  const { role } = api.message().getState();
}
```

### Accessing Nested Scopes

Navigate through the scope hierarchy programmatically:

```tsx
const api = useAssistantApi();

// Access specific message by ID or index
const messageById = api.thread().message({ id: "msg_123" });
const messageByIndex = api.thread().message({ index: 0 });

// Access part by index or tool call ID
const partByIndex = api.message().part({ index: 0 });
const partByToolCall = api.message().part({ toolCallId: "call_123" });

// Access attachment by index
const attachment = api.composer().attachment({ index: 0 }).getState();

// Access thread from thread list
const thread = api.threads().thread("main");
const threadItem = api.threads().item({ id: "thread_123" });
```

## Common Patterns

### Conditional Rendering

```tsx
function RunIndicator() {
  const isRunning = useAssistantState(({ thread }) => thread.isRunning);

  if (!isRunning) return null;
  return <div>Assistant is thinking...</div>;
}
```

### Custom Action Buttons

```tsx
function CopyButton() {
  const api = useAssistantApi();

  const handleCopy = () => {
    navigator.clipboard.writeText(api.message().getCopyText());
  };

  return <button onClick={handleCopy}>Copy</button>;
}
```

### State-Aware Components

```tsx
function SmartComposer() {
  const api = useAssistantApi();
  const isRunning = useAssistantState(({ thread }) => thread.isRunning);
  const text = useAssistantState(({ composer }) => composer.text);

  const canSend = !isRunning && text.length > 0;

  return (
    <div>
      <textarea
        value={text}
        onChange={(e) => api.composer().setText(e.target.value)}
        disabled={isRunning}
      />
      <button onClick={() => api.composer().send()} disabled={!canSend}>
        Send
      </button>
    </div>
  );
}
```

### Event-Driven Updates

```tsx
function MessageCounter() {
  const [sendCount, setSendCount] = useState(0);

  useAssistantEvent("composer.send", () => {
    setSendCount((c) => c + 1);
  });

  return <div>Messages sent: {sendCount}</div>;
}
```

## Advanced Topics

### Resolution Dynamics

When you call `api.scope()`, the API resolves the current scope at that moment. This resolution happens each time you call the function, which matters when dealing with changing contexts:

```tsx
const api = useAssistantApi();

// Get current thread
const thread1 = api.thread();
thread1.append({ role: "user", content: "Hello" });

// User might switch threads here

// This could be a different thread
const thread2 = api.thread();
thread2.cancelRun(); // Cancels the current thread's run, not necessarily thread1's
```

For most use cases, this behavior is intuitive. In advanced scenarios where you need to track specific instances, store the resolved reference.

### Performance Optimization

**Selector optimization:**

```tsx
// ❌ Expensive computation in selector (runs on every store update)
const result = useAssistantState(
  ({ thread }) => thread.messages.filter((m) => m.role === "user").length,
);

// ✅ Memoize expensive computations
const messages = useAssistantState(({ thread }) => thread.messages);
const userCount = useMemo(
  () => messages.filter((m) => m.role === "user").length,
  [messages],
);
```

**Minimize re-renders:**

```tsx
// ❌ Subscribes to entire thread state
const thread = useAssistantState(({ thread }) => thread);

// ✅ Subscribe only to needed values
const isRunning = useAssistantState(({ thread }) => thread.isRunning);
```

## API Reference

### Hooks

| Hook                                | Purpose                    | Returns        |
| ----------------------------------- | -------------------------- | -------------- |
| `useAssistantState(selector)`       | Subscribe to state changes | Selected value |
| `useAssistantApi()`                 | Get API instance           | API object     |
| `useAssistantEvent(event, handler)` | Subscribe to events        | void           |

### Scope States

| Scope          | Key State Properties                                                              | Description                                      |
| -------------- | --------------------------------------------------------------------------------- | ------------------------------------------------ |
| ThreadList     | `mainThreadId`, `threadIds`, `isLoading`, `threadItems`                           | Manages all available conversation threads       |
| ThreadListItem | `id`, `title`, `status`, `remoteId`, `externalId`                                 | Individual thread metadata and status            |
| Thread         | `isRunning`, `isLoading`, `isDisabled`, `messages`, `capabilities`, `suggestions` | Active conversation state and message history    |
| Message        | `role`, `content`, `status`, `attachments`, `parentId`, `branchNumber`, `isLast`  | Individual message content and metadata          |
| Composer       | `text`, `role`, `attachments`, `isEmpty`, `canCancel`, `type`, `isEditing`        | Text input state for new/edited messages         |
| Part           | `type`, `content`, `status`, `text`, `toolCallId`, `toolName`                     | Content parts within messages (text, tool calls) |
| Attachment     | `id`, `type`, `name`, `url`, `size`, `mimeType`                                   | File attachments metadata and content            |

### Available Actions by Scope

| Scope          | Actions                                                               | Use Cases                                 |
| -------------- | --------------------------------------------------------------------- | ----------------------------------------- |
| ThreadList     | `switchToNewThread()`, `switchToThread(id)`, `getState()`             | Thread navigation and creation            |
| ThreadListItem | `switchTo()`, `rename(title)`, `archive()`, `unarchive()`, `delete()` | Thread management operations              |
| Thread         | `append(message)`, `startRun()`, `cancelRun()`, `switchToNewThread()` | Message handling and conversation control |
| Message        | `reload()`, `speak()`, `stopSpeaking()`, `submitFeedback(feedback)`   | Message interactions and regeneration     |
| Composer       | `send()`, `setText(text)`, `addAttachment(file)`, `reset()`           | Text input and message composition        |
| Part           | `addResult(result)`, `getState()`                                     | Tool call result handling                 |
| Attachment     | `remove()`, `getState()`                                              | File management                           |

### Common Events

| Event                            | Description                   |
| -------------------------------- | ----------------------------- |
| `thread.run-start`               | Assistant starts generating   |
| `thread.run-end`                 | Assistant finishes generating |
| `thread.initialize`              | Thread is initialized         |
| `thread.model-context-update`    | Model context is updated      |
| `composer.send`                  | Message is sent               |
| `composer.attachment-add`        | Attachment added to composer  |
| `thread-list-item.switched-to`   | Switched to a thread          |
| `thread-list-item.switched-away` | Switched away from a thread   |

## Troubleshooting

### Common Errors

**"Cannot access [scope] outside of [scope] context"**

```tsx
// ❌ This will throw if not inside a message component
const role = useAssistantState(({ message }) => message.role);

// ✅ Check scope availability first
function SafeMessageButton() {
  const api = useAssistantApi();

  const role = useAssistantState(({ message }) =>
    api.message.source !== undefined ? message.role : "none",
  );

  return <div>Role: {role}</div>;
}
```

**"Maximum update depth exceeded" / Infinite re-renders**

```tsx
// ❌ Creating new objects in selectors causes infinite re-renders
const data = useAssistantState(({ message }) => ({
  role: message.role,
  content: message.content, // New object every time!
}));

// ✅ Return primitive values or use separate selectors
const role = useAssistantState(({ message }) => message.role);
const content = useAssistantState(({ message }) => message.content);
```

**"Scope resolution failed" / Stale scope references**

```tsx
// ❌ Storing scope references can lead to stale data
const api = useAssistantApi();
const thread = api.thread(); // This reference might become stale

useEffect(() => {
  // This might reference the wrong thread if user switched
  thread.cancelRun();
}, [thread]);

// ✅ Resolve scopes fresh each time
const api = useAssistantApi();

useEffect(() => {
  // Always gets the current thread
  api.thread().cancelRun();
}, [api]);
```

## Quick Reference

```tsx
// Read state
const value = useAssistantState(({ scope }) => scope.property);

// Perform action
const api = useAssistantApi();
api.scope().action();

// Listen to events
useAssistantEvent("source.event", (e) => {});

// Check scope availability
if (api.scope.source) {
  /* scope exists */
}

// Get state imperatively
const state = api.scope().getState();

// Navigate scopes
api.thread().message({ id: "..." }).getState();
```
